/*
  Copyright 2012 Sergey Ostanin

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

package ru.irenproject;

import java.util.ArrayList;
import java.util.List;

import javax.inject.Inject;

import org.apache.commons.lang3.Validate;

import ru.irenproject.TestProtos.SectionNode;
import ru.irenproject.store.AbstractThing;
import ru.irenproject.store.Event;
import ru.irenproject.store.Stash;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterators;
import com.google.common.collect.UnmodifiableIterator;
import com.google.protobuf.ByteString;

public final class SectionTree extends AbstractThing implements Iterable<Section> {
  public enum ChangeEvent implements Event { INSTANCE }

  @Stash private TestProtos.SectionTree.Builder $;

  @Inject SectionTree() {}

  @Override protected void initialize() {
    Section rootSection = store().create(Section.class);
    $.setRoot(SectionNode.newBuilder().setSection(rootSection.id()));
  }

  public void add(Section section, Section parent) {
    getNode(parent).addChildBuilder().setSection(section.id());
    post(ChangeEvent.INSTANCE);
  }

  private SectionNode.Builder getNode(Section section) {
    List<SectionNode.Builder> path = getPathInternal(section);
    Validate.isTrue(!path.isEmpty());
    return path.get(path.size() - 1);
  }

  public Section rootSection() {
    return get($.getRoot().getSection(), Section.class);
  }

  public SectionNode rootNode() {
    return $.getRoot();
  }

  public boolean contains(Section section) {
    return !getPathInternal(section).isEmpty();
  }

  public void remove(Section section) {
    TestCheck.input(section != rootSection());

    List<SectionNode.Builder> path = getPathInternal(section);
    SectionNode.Builder node = path.get(path.size() - 1);
    SectionNode.Builder parentNode = path.get(path.size() - 2);
    parentNode.removeChild(parentNode.getChildBuilderList().indexOf(node));
    post(ChangeEvent.INSTANCE);
  }

  public List<Section> getPath(Section section) {
    List<Section> res = new ArrayList<>();
    for (SectionNode.Builder b : getPathInternal(section)) {
      res.add(get(b.getSection(), Section.class));
    }
    return res;
  }

  private List<SectionNode.Builder> getPathInternal(Section section) {
    return getPathInternal(section.id());
  }

  private List<SectionNode.Builder> getPathInternal(ByteString sectionId) {
    List<SectionNode.Builder> res = new ArrayList<>();
    res.add($.getRootBuilder());
    getPathInternal(sectionId, res);
    return res;
  }

  private boolean getPathInternal(ByteString sectionId, List<SectionNode.Builder> path) {
    SectionNode.Builder last = path.get(path.size() - 1);
    if (last.getSection().equals(sectionId)) {
      return true;
    }

    for (SectionNode.Builder b : last.getChildBuilderList()) {
      path.add(b);
      if (getPathInternal(sectionId, path)) {
        return true;
      }
    }

    path.remove(path.size() - 1);
    return false;
  }

  public void move(Section section, boolean forward) {
    List<SectionNode.Builder> path = getPathInternal(section);
    TestCheck.input(path.size() >= 2);

    SectionNode.Builder s = path.get(path.size() - 1);
    SectionNode.Builder parent = path.get(path.size() - 2);
    int index = indexOfSectionId(s.getSection(), parent);

    SectionNode.Builder grandparent = null;
    int parentIndex = -1;
    if (path.size() >= 3) {
      grandparent = path.get(path.size() - 3);
      parentIndex = indexOfSectionId(parent.getSection(), grandparent);
    }

    parent.removeChild(index);

    if (forward) {
      if (index < parent.getChildCount()) {
        parent.getChildBuilder(index).addChild(0, s);
      } else {
        TestCheck.input(grandparent != null);
        grandparent.addChild(parentIndex + 1, s);
      }
    } else {
      if (index > 0) {
        parent.getChildBuilder(index - 1).addChild(s);
      } else {
        TestCheck.input(grandparent != null);
        grandparent.addChild(parentIndex, s);
      }
    }

    post(ChangeEvent.INSTANCE);
  }

  private static int indexOfSectionId(ByteString sectionId, SectionNode.Builder parent) {
    int n = parent.getChildCount();
    for (int i = 0; i < n; ++i) {
      if (parent.getChildBuilder(i).getSection().equals(sectionId)) {
        return i;
      }
    }
    throw new RuntimeException();
  }

  public void removeLeaves(Predicate<Section> predicate) {
    removeLeavesInternal($.getRootBuilder(), predicate);
    post(ChangeEvent.INSTANCE);
  }

  private void removeLeavesInternal(SectionNode.Builder node, Predicate<Section> predicate) {
    for (int i = node.getChildCount() - 1; i >= 0; --i) {
      SectionNode.Builder child = node.getChildBuilder(i);
      removeLeavesInternal(child, predicate);
      if (child.getChildCount() == 0) {
        Section childSection = get(child.getSection(), Section.class);
        if (predicate.apply(childSection)) {
          node.removeChild(i);
        }
      }
    }
  }

  private List<Section> sectionList() {
    List<Section> res = new ArrayList<>();
    getSubtree(rootNode(), res);
    return res;
  }

  private void getSubtree(SectionNode parent, List<Section> out) {
    out.add(get(parent.getSection(), Section.class));
    for (SectionNode child : parent.getChildList()) {
      getSubtree(child, out);
    }
  }

  @Override public UnmodifiableIterator<Section> iterator() {
    return Iterators.unmodifiableIterator(sectionList().iterator());
  }
}
